Почему «while (! Feof (file))» всегда неверно? - PullRequest
525 голосов
/ 25 марта 2011

В последнее время я видел людей, которые пытались читать такие файлы во многих сообщениях:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    char *path = argc > 1 ? argv[1] : "input.txt";

    FILE *fp = fopen(path, "r");
    if(fp == NULL)
    {
        perror(path);
        return EXIT_FAILURE;
    }

    while(!feof(fp))
    {
        /* Read and process data from file… */
    }
    if(fclose(fp) == 0)
    {
        return EXIT_SUCCESS;
    }
    else
    {
        perror(path);
        return EXIT_FAILURE;
    }
}

Что не так с этим циклом while(!feof(fp))?

Ответы [ 5 ]

424 голосов
/ 25 октября 2014

Я хотел бы представить абстрактную перспективу высокого уровня.

Параллельность и одновременность

Операции ввода-вывода взаимодействуют со средой. Среда не является частью вашей программы и не находится под вашим контролем. Среда действительно существует "одновременно" с вашей программой. Как и в случае с другими вещами, вопросы о «текущем состоянии» не имеют смысла: не существует понятия «одновременности» между одновременными событиями. Многие свойства состояния просто не существуют одновременно.

Позвольте мне уточнить это: предположим, вы хотите спросить: «У вас есть больше данных». Вы можете запросить это у параллельного контейнера или вашей системы ввода-вывода. Но ответ, как правило, бездействующий и, следовательно, бессмысленный. Так что, если контейнер говорит «да» - ndash; к тому времени, когда вы попытаетесь прочитать, у него больше не будет данных. Точно так же, если ответ «нет», к тому времени, когда вы попытаетесь прочитать, данные, возможно, уже поступили. Вывод заключается в том, что просто не является таким свойством, как «У меня есть данные», поскольку вы не можете действовать осмысленно в ответ на любой возможный ответ. (Ситуация несколько лучше с буферизованным вводом, где вы можете получить «да, у меня есть данные», что является своего рода гарантией, но вам все равно придется иметь дело с противоположным случаем. И с выводом ситуации все так же плохо, как я описал: вы никогда не знаете, заполнен ли этот диск или сетевой буфер.)

Таким образом, мы приходим к выводу, что невозможно и на самом деле разумно спросить систему ввода-вывода, сможет ли она выполнить операцию ввода-вывода. Единственный возможный способ взаимодействия с ним (так же, как с параллельным контейнером) - это попытка операции и проверка ее успешности или неудачи. В тот момент, когда вы взаимодействуете со средой, тогда и только тогда вы можете узнать, действительно ли взаимодействие было возможно, и в этот момент вы должны посвятить себя выполнению взаимодействия. (Это «точка синхронизации», если хотите.)

EOF

Теперь мы добрались до EOF. EOF - это ответ , полученный при попытке операции ввода / вывода. Это означает, что вы пытались что-то прочитать или записать, но при этом вам не удалось прочитать или записать какие-либо данные, и вместо этого был обнаружен конец ввода или вывода. Это верно практически для всех API ввода-вывода, будь то стандартная библиотека C, iostreams C ++ или другие библиотеки. Пока операции ввода-вывода завершаются успешно, вы просто не можете знать , будут ли дальнейшие операции успешными. Вы должны всегда сначала пытаться выполнить операцию, а затем реагировать на успех или неудачу.

Примеры

В каждом из примеров обратите внимание на то, что мы сначала предпринимаем попытку операции ввода-вывода, а , а затем потребляют результат, если он действителен. Отметим далее, что мы всегда должны использовать результат операции ввода-вывода, хотя результат принимает разные формы и формы в каждом примере.

  • C stdio, чтение из файла:

    for (;;) {
        size_t n = fread(buf, 1, bufsize, infile);
        consume(buf, n);
        if (n < bufsize) { break; }
    }
    

    Результат, который мы должны использовать, равен n, числу прочитанных элементов (которое может быть равным нулю).

  • C stdio, scanf:

    for (int a, b, c; scanf("%d %d %d", &a, &b, &c) == 3; ) {
        consume(a, b, c);
    }
    

    Результат, который мы должны использовать, это возвращаемое значение scanf, количество преобразованных элементов.

  • C ++, извлечение в формате iostreams:

    for (int n; std::cin >> n; ) {
        consume(n);
    }
    

    Результат, который мы должны использовать, - это std::cin, который можно оценить в логическом контексте и сообщить нам, находится ли поток в состоянии good().

  • C ++, iostreams getline:

    for (std::string line; std::getline(std::cin, line); ) {
        consume(line);
    }
    

    Результат, который мы должны использовать, снова std::cin, как и прежде.

  • POSIX, write(2) для очистки буфера:

    char const * p = buf;
    ssize_t n = bufsize;
    for (ssize_t k = bufsize; (k = write(fd, p, n)) > 0; p += k, n -= k) {}
    if (n != 0) { /* error, failed to write complete buffer */ }
    

    Результат, который мы здесь используем, равен k, числу записанных байтов. Дело в том, что мы можем знать только, сколько байтов было записано после операции записи.

  • POSIX getline()

    char *buffer = NULL;
    size_t bufsiz = 0;
    ssize_t nbytes;
    while ((nbytes = getline(&buffer, &bufsiz, fp)) != -1)
    {
        /* Use nbytes of data in buffer */
    }
    free(buffer);
    

    Результат, который мы должны использовать: nbytes, число байтов до и включая символ новой строки (или EOF, если файл не заканчивался символом новой строки).

    Обратите внимание, что функция явно возвращает -1 (а не EOF!), Когда возникает ошибка или она достигает EOF.

Вы можете заметить, что мы очень редко произносим слово "EOF". Обычно мы обнаруживаем состояние ошибки другим способом, который нам более интересен (например, невозможность выполнить столько операций ввода-вывода, сколько мы хотели). В каждом примере есть некоторая функция API, которая может явно сообщить нам, что с состоянием EOF было обнаружено, но на самом деле это не очень полезная часть информации. Это гораздо больше деталей, чем мы часто заботимся. Важно то, был ли ввод / вывод успешным, в большей степени, чем как он провалился.

  • Последний пример, который фактически запрашивает состояние EOF: предположим, у вас есть строка и вы хотите проверить, что она представляет целое число полностью, без дополнительных битов в конце, кроме пробелов. Используя C ++ iostreams, это выглядит так:

    std::string input = "   123   ";   // example
    
    std::istringstream iss(input);
    int value;
    if (iss >> value >> std::ws && iss.get() == EOF) {
        consume(value);
    } else {
        // error, "input" is not parsable as an integer
    }
    

    Мы используем два результата здесь. Первый - это iss, сам объект потока, для проверки успешности форматированного извлечения в value. Но затем, после использования пустого пространства, мы выполняем еще одну операцию ввода-вывода, iss.get(), и ожидаем, что она завершится с ошибкой как EOF, что является случаем, если вся строка уже была использована форматированным извлечением.

    В стандартной библиотеке C вы можете добиться чего-то похожего с функциями strto*l, проверив, что указатель конца достиг конца входной строки.

Ответ

while(!eof) неправильно, потому что он проверяет что-то, что не имеет отношения к делу и не может проверить то, что вам нужно знать. В результате вы ошибочно выполняете код, который предполагает, что он обращается к данным, которые были успешно прочитаны, хотя на самом деле этого никогда не происходило.

219 голосов
/ 25 марта 2011

Это неправильно, потому что (при отсутствии ошибки чтения) он входит в цикл еще раз, чем ожидает автор. Если есть ошибка чтения, цикл никогда не завершается.

Рассмотрим следующий код:

/* WARNING: demonstration of bad coding technique!! */

#include <stdio.h>
#include <stdlib.h>

FILE *Fopen(const char *path, const char *mode);

int main(int argc, char **argv)
{
    FILE *in;
    unsigned count;

    in = argc > 1 ? Fopen(argv[1], "r") : stdin;
    count = 0;

    /* WARNING: this is a bug */
    while (!feof(in)) {  /* This is WRONG! */
        fgetc(in);
        count++;
    }
    printf("Number of characters read: %u\n", count);
    return EXIT_SUCCESS;
}

FILE * Fopen(const char *path, const char *mode)
{
    FILE *f = fopen(path, mode);
    if (f == NULL) {
        perror(path);
        exit(EXIT_FAILURE);
    }
    return f;
}

Эта программа будет последовательно печатать на единицу больше, чем количество символов во входном потоке (при условии отсутствия ошибок чтения). Рассмотрим случай, когда входной поток пуст:

$ ./a.out < /dev/null
Number of characters read: 1

В этом случае, feof() вызывается до того, как какие-либо данные были прочитаны, поэтому возвращает false. Цикл вводится, вызывается fgetc() (и возвращает EOF), и счет увеличивается. Затем вызывается feof() и возвращается значение true, что приводит к прерыванию цикла.

Это происходит во всех таких случаях. feof() не возвращает true, пока после чтение потока не встретит конец файла. Цель feof() - НЕ проверять, достигнет ли следующее чтение конца файла. Цель feof() - провести различие между ошибкой чтения и достижением конца файла. Если fread() возвращает 0, вы должны использовать feof / ferror для принятия решения. Аналогично, если fgetc возвращает EOF. feof() полезно только после fread вернул ноль или fgetc вернул EOF. До этого feof() всегда будет возвращать 0.

Всегда необходимо проверять возвращаемое значение чтения (или fread(), или fscanf(), или fgetc()) перед вызовом feof().

Еще хуже, рассмотрим случай, когда происходит ошибка чтения. В этом случае fgetc() возвращает EOF, feof() возвращает false, и цикл никогда не завершается. Во всех случаях, когда используется while(!feof(p)), должна быть, по крайней мере, проверка внутри цикла для ferror(), или, по крайней мере, условие while должно быть заменено на while(!feof(p) && !ferror(p)), или существует очень реальная возможность бесконечного цикл, вероятно, извергает все виды мусора, поскольку обрабатываются недействительные данные.

Итак, в заключение, хотя я не могу с уверенностью утверждать, что никогда не бывает ситуации, в которой может быть семантически правильным написать «while(!feof(f))» (хотя должен быть другой проверкой внутри цикла с разрывом, чтобы избежать бесконечного цикла при ошибке чтения), это почти всегда неверно. И даже если когда-либо возникнет случай, когда он будет правильным, это настолько идиоматически неправильно, что это не будет правильным способом написания кода. Любой, кто увидит этот код, должен сразу же подумать и сказать: «Это ошибка». И, возможно, ударить автора (если автор не ваш начальник, в этом случае рекомендуется усмотрение.)

60 голосов
/ 25 марта 2011

Нет, это не всегда неправильно.Если ваше условие цикла «пока мы не пытались прочитать конец файла», тогда вы используете while (!feof(f)).Это, однако, не является общим условием цикла - обычно вы хотите проверить что-то еще (например, «могу я прочитать больше»)while (!feof(f)) не так, просто используется неправильно.

31 голосов
/ 10 февраля 2012

feof() указывает, пытался ли кто-нибудь прочитать после конца файла. Это означает, что он имеет небольшой прогнозирующий эффект: если он истинен, вы уверены, что следующая операция ввода потерпит неудачу (вы не уверены, что предыдущая провалилась, кстати,), но если она ложна, вы не уверены, что следующий ввод операция будет успешной. Более того, операции ввода могут завершаться неудачей по другим причинам, кроме конца файла (ошибка форматирования для форматированного ввода, ошибка чистого ввода-вывода - сбой диска, тайм-аут сети - для всех типов ввода), поэтому даже если вы можете предвидеть конец файла (и любой, кто пытался внедрить Ada one, который является прогностическим, скажет вам, что он может быть сложным, если вам нужно пропустить пробелы, и что это оказывает нежелательное влияние на интерактивные устройства - иногда вынуждая вводить следующее перед началом обработки предыдущего), вы должны быть в состоянии обработать сбой.

Таким образом, правильная идиома в C состоит в том, чтобы выполнить цикл с успешным выполнением операции ввода-вывода в качестве условия цикла, а затем проверить причину сбоя. Например:

while (fgets(line, sizeof(line), file)) {
    /* note that fgets don't strip the terminating \n, checking its
       presence allow to handle lines longer that sizeof(line), not showed here */
    ...
}
if (ferror(file)) {
   /* IO failure */
} else if (feof(file)) {
   /* format error (not possible with fgets, but would be with fscanf) or end of file */
} else {
   /* format error (not possible with fgets, but would be with fscanf) */
}
10 голосов
/ 02 июня 2013

Отличный ответ, я просто заметил то же самое, потому что пытался сделать такой цикл. Так что это неправильно в этом сценарии, но если вы хотите иметь цикл, который изящно заканчивается в EOF, это хороший способ сделать это:

#include <stdio.h>
#include <sys/stat.h>
int main(int argc, char *argv[])
{
  struct stat buf;
  FILE *fp = fopen(argv[0], "r");
  stat(filename, &buf);
  while (ftello(fp) != buf.st_size) {
    (void)fgetc(fp);
  }
  // all done, read all the bytes
}
...